package br.ufrj.dcc.tp.cliente.model;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.List;

import br.ufrj.dcc.tp.cliente.controller.ClientController;
import br.ufrj.dcc.tp.cliente.model.exception.ClientException;
import br.ufrj.dcc.tp.utils.SocketAction;
import br.ufrj.dcc.tp.utils.SocketMessageImpl;
import br.ufrj.dcc.tp.utils.interfaces.ISocketMessage;
/**
 * Classe que representa um cliente.
 *
 */
public class Client {
	
	/*** Socket */
	private Socket socket;
	
	/** Entrada de mensagens do socket. */
	private ObjectInputStream in;
	/** Saída de mensagens para o socket. */
	private ObjectOutputStream out;
	
	private String clientName;
	
	private ClientNotifier notifier;
	
	/** Controller do cliente. */
	private ClientController controller;
	
	public Client(ClientController controller){
		this.controller = controller;
	}
	
	public String getClientName() {
		return clientName;
	}
	
	/** Conecta no servidor especificado. */
	public void connectToServer() throws ClientException, IOException, ClassNotFoundException  {
		try {
			ISocketMessage inputMessage = null;
			clientName = controller.getClientName();
			
			String ip = controller.getServerIP();
			if( ip == null ) {
				throw new ClientException("IP invalido.");
			}
			
			Integer port = controller.getServerPort();
			if( port == null ) {
				throw new ClientException("Porta invalida.");
			}
			
			if( clientName == null ) {
				throw new ClientException("Nome invalido");
			}
			
			socket = new Socket(ip, port);
			in = new ObjectInputStream(socket.getInputStream());
			out = new ObjectOutputStream(socket.getOutputStream());
			
			if( ! socket.isBound() || ! socket.isConnected() ) {
				throw new ClientException("Nao foi possivel conectar.");
			}
			
			// Comeca o Handeshake
			out.writeObject(
				new SocketMessageImpl(clientName, SocketAction.CREATE)
			);
			Object obj = in.readObject();
			if( obj == null || !( obj instanceof SocketMessageImpl) ) {
				throw new ClientException("Erro ao receber mensagem de resposta do servidor.");
			}
			
			inputMessage = (SocketMessageImpl) obj;
			
			// Verificar se voltou a mesma mensagem
			if( inputMessage.getAction() != SocketAction.CREATE ) {
				throw new ClientException("Erro de comunicacao com servidor.");
			} else if( ! inputMessage.getSender().equals(clientName) ) {
				throw new ClientException("Nome de usuario ja existente.");
			}

			notifier = new ClientNotifier(controller, socket, in);
			
			out.writeObject(
				new SocketMessageImpl(clientName, SocketAction.LIST_USERS)
			);
			out.flush();
			
			notifier.start();
		} catch(IOException e ) {
			closeConnection();
			throw e;
		} catch(ClassNotFoundException e) {
			closeConnection();
			throw new ClientException("Erro de protocolo.");
		}
	}
	
	public void forceClose() throws IOException {
		try {
			if( notifier != null && notifier.isAlive() ) {
				notifier.interrupt();
				notifier.join();
			}
		} catch (InterruptedException e) {
		}
		
		if(socket == null ) {
			return;
		}
		
		try {
			synchronized (socket) {
				System.out.println("forceClose" + getClientName());
				out.flush();
				out.close();
				in.close();
				socket.close();
			}
		} catch(IOException ioe) {
		}
	}
	
	/** Encerra a conexão. */
	public void closeConnection() throws IOException {
		try {
			if( notifier != null && notifier.isAlive() ) {
				notifier.interrupt();
				notifier.join();
			}
		} catch (InterruptedException e) {
		}
		
		if(socket == null ) {
			return;
		}
		
		try {
			synchronized (socket) {
				// Nesse ponto, nao da pra saber se o servidor desconectou primeiro
				// O jeito eh testar escrevendo.
				System.out.println("close" + getClientName());
				if( ! socket.isClosed() && ! socket.isOutputShutdown() ) {
					out.writeObject(
						new SocketMessageImpl(clientName, SocketAction.CLOSE)
					);
				}
				out.flush();
				out.close();
				in.close();
				socket.close();
			}
		} catch(IOException ioe) {
			forceClose();
			// ignorada, nesse caso não dá pra saber
			// se o outro lado do socket fechou.
		}
	}
	
	/** Envia a mensagem pelo socket */
	public void sendMessage(List<String> recipients, String message) throws IOException {
		
		if( recipients.size() == 0 ) {
			recipients = null;
		}
		
		synchronized (socket) {
			if(socket != null && ! socket.isOutputShutdown() && ! socket.isClosed()) {
				out.writeObject(
					new SocketMessageImpl(clientName, SocketAction.SEND_MESSAGE, recipients, message)
				);
				out.flush();
			}
		}
	}
}
